package org.ytu.classifier.kmeans;

import org.ytu.classifier.beans.AnalyseBean;
import org.ytu.classifier.beans.KmeansResultBean;
import org.ytu.classifier.fileIO.AlgorithmOutput;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Kmeans {
   private int k;
   private int size;//feature size -> cepstralCoefficients sayısı
   private List<FeaturedData> allList;
   private Cluster[] clusters;
   private boolean centroidsChanged = false;
   private Map[] resultMaps;// Hangi kümede hangi elemandan kaç tane olduğunu tutar
   private double[] reconstructionErrors;// Her bir kümenin reconstruction error'ünü tutar
   private double[] maxValues;// Random başlangıç centroid'leri ataması için kullanılacak
   private double[] minValues;// Random başlangıç centroid'leri ataması için kullanılacak
   private AlgorithmOutput logToFile;//Algoritma sonucunu dosyaya yazmaktan sorumlu değişken
   private int numberOfFilters;//Bu değişken sadece log dosyasına yazılmak için vardır, algoritmada kullanılmamaktadır
   private int animalCount;//Ortamdaki hayvan sayısını tutmaktadır, algoritmada kullanılmamaktadır
   private boolean isMel;//Algoritmanın MFCC mi yoksa BFCC için mi çalıştırıldığını tutar

   public Kmeans(int k, List<FeaturedData> allList, int size, int numberOfFilters, int animalCount, boolean isMel) {
      this.k = k;
      this.allList = allList;
      this.size = size;
      clusters = new Cluster[k];
      for (int i = 0; i < k; i++) {
         clusters[i] = new Cluster("Küme " + i);
      }
      initMinMaxArrays();
      initMinMaxDimensions();
      this.numberOfFilters = numberOfFilters;
      this.animalCount = animalCount;
      this.isMel = isMel;
      logToFile = new AlgorithmOutput("C:\\Users\\Furkan\\Desktop\\kmeansLog.txt");
   }

   public AnalyseBean startAlgorithm() {
      initRandomCentroids();
      //initCentroids();
      do {
         assignElementsToClusters();
         rearrangeCentroids();
      } while (centroidsChanged);
      confusionMatrix();
      reconstructionError();
      //Algoritma sonucunda oluşan parametreler toplanıyor
      KmeansResultBean algorithmResult = new KmeansResultBean();
      //Sounuç parametreleri değişkene kaydediliyor
      setFromResultBean(algorithmResult);
      //Dosyaya algoritma çıktısı yazdırılıyor
      return logToFile.logKmeansResult(algorithmResult);
   }

   //todo bu metod random başlangıç noktası atamaya test amaçlı, kaldırılabilir
   public void initRandomCentroids() {
      Centroid centroid;
      for (int i = 0; i < k; i++) {
         centroid = new Centroid(size);
         double[] centroidFeatures = new double[size];
         for (int j = 0; j < size; j++) {
            centroidFeatures[j] = Math.random() * (maxValues[j] - minValues[j]) + minValues[j];
         }
         centroid.setFeatures(centroidFeatures);
         clusters[i].setCentroid(centroid);
      }
   }

   public void initCentroids() {
      Centroid centroid;
      for (int i = 0; i < k; i++) {
         centroid = new Centroid(size);
         double[] centroidFeatures = new double[size];
         for (int j = 0; j < size; j++) {
            centroidFeatures[j] = ((maxValues[j] - minValues[j]) / (k + 1) * i) + minValues[j];
         }
         centroid.setFeatures(centroidFeatures);
         clusters[i].setCentroid(centroid);
      }
   }

   public void assignElementsToClusters() {
      initClusterElements();
      double minDistance;
      int index;
      for (FeaturedData data : allList) {
         minDistance = Double.MAX_VALUE;
         index = 0;
         for (int i = 0; i < k; i++) {
            double distanceToCendroid = calculateDistances(data.getFeatures(), clusters[i].getCentroid().getFeatures());
            if (distanceToCendroid < minDistance) {
               minDistance = distanceToCendroid;
               index = i;
            }
         }
         clusters[index].addElementToCluster(data);//todo her cluster'ı temizlemek yerine noktaları cluster'lar arasında kaydır
      }
   }

   //Öklid uzaklığını kare kökü almadan toplamlar halinde döndürür
   public double calculateDistances(double[] value1, double[] value2) {
      double sumOfSquares = 0;
      for (int i = 0; i < size; i++) {
         sumOfSquares += Math.pow(value1[i] - value2[i], 2);
      }
      return sumOfSquares;
   }

   public void rearrangeCentroids() {
      centroidsChanged = false;
      for (int i = 0; i < k; i++) {
         Centroid centroid = new Centroid(size);
         double[] centroidFeatures = new double[size];
         List<FeaturedData> dataList = clusters[i].getDataList();
         for (FeaturedData data : dataList) {
            double[] features = data.getFeatures();
            for (int index = 0; index < size; index++) {
               centroidFeatures[index] += features[index] / dataList.size();
            }
         }
         centroid.setFeatures(centroidFeatures);
         if (centroid.hashCode() != clusters[i].getCentroid().hashCode()) {
            centroidsChanged = true;
         }
         clusters[i].setCentroid(centroid);
      }
   }

   public void confusionMatrix() {
      resultMaps = new Map[k];
      for (int index = 0; index < clusters.length; index++) {
         Map<String, Integer> resultMap = new HashMap<String, Integer>();
         List<FeaturedData> clusterElements = clusters[index].getDataList();
         for (FeaturedData data : clusterElements) {
            String animalSpecies = data.getName();
            if (!resultMap.containsKey(animalSpecies)) {
               resultMap.put(animalSpecies, 1);
            } else {
               Integer count = resultMap.get(animalSpecies) + 1;
               resultMap.remove(animalSpecies);
               resultMap.put(animalSpecies, count);
            }
         }
         resultMaps[index] = resultMap;
      }
   }

   public void reconstructionError() {
      double sumOfSquares;
      reconstructionErrors = new double[k];
      for (int i = 0; i < clusters.length; i++) {
         List<FeaturedData> clusterElements = clusters[i].getDataList();
         double error = 0;
         for (FeaturedData data : clusterElements) {
            sumOfSquares = calculateDistances(data.getFeatures(), clusters[i].getCentroid().getFeatures());
            error += Math.pow(sumOfSquares, 1.0 / size * 1.0);
         }
         reconstructionErrors[i] = error;
      }
   }

   public void initMinMaxArrays() {
      maxValues = new double[size];
      minValues = new double[size];
      for (int i = 0; i < size; i++) {
         maxValues[i] = Double.MIN_VALUE;
         minValues[i] = Double.MAX_VALUE;
      }
   }

   public void initMinMaxDimensions() {
      for (FeaturedData frameFeature : allList) {
         double[] features = frameFeature.getFeatures();
         for (int i = 0; i < features.length; i++) {
            if (features[i] > maxValues[i]) {
               maxValues[i] = features[i];
            }
            if (features[i] < minValues[i]) {
               minValues[i] = features[i];
            }
         }
      }
   }

   public void initClusterElements() {
      List<FeaturedData> emptyList;
      for (int i = 0; i < k; i++) {
         emptyList = new ArrayList<FeaturedData>();
         clusters[i].setDataList(emptyList);
      }
   }

   public void setFromResultBean(KmeansResultBean algorithmResult) {
      algorithmResult.setK(this.k);
      algorithmResult.setCepstralCoefficients(this.size);
      algorithmResult.setNumberOfFilters(this.numberOfFilters);
      algorithmResult.setReconstructionErrors(this.reconstructionErrors);
      algorithmResult.setResultMaps(this.resultMaps);
      algorithmResult.setAnimalCount(this.animalCount);
      algorithmResult.setMel(this.isMel);
   }
}
